#include "scene.h"
#include <logger/logger.h>
#include <limits>

#include <qmath.h>

#include <QTimer>
#include <QtWebKit>
#include <QGLWidget>
#include <QVector3D>
#include <QPushButton>


#include "common.h"
#include "entity.h"

#include <QtOpenGL>

#include "graphicsentity.h"
#include "helpers/oglhelper.h"
#include "helpers/glextensions.h"
#include "helpers/glbuffers.h"
#include "trackball.h"
#include "helpers/roundedbox.h"
#include "3rdparty/fbm.h"


#include "textures.h"
#define PI 3.14159265358979

namespace Opl{ namespace Gui {


        const static char environmentShaderText[] =
                "uniform samplerCube env;"
                "void main() {"
                "gl_FragColor = textureCube(env, gl_TexCoord[1].xyz);"
                "}";


        //TODO:move to common



		class ScenePrivate{
		public:
			ScenePrivate(): m_distExp(600)
					, frame(0)
					, m_currentShader(3)
					, m_currentTexture(0)
					, m_dynamicCubemap(false)
					, m_updateAllCubemaps(true)
					, m_box(0)
					, m_vertexShader(0)
					, m_environmentShader(0)
					, m_environmentProgram(0)
			{}

			~ScenePrivate()
			{
				if (m_box)
					delete m_box;
//				foreach (GLTexture *texture, m_textures)
//					if (texture) delete texture;
				if (m_mainCubemap)
					delete m_mainCubemap;
				foreach (QGLShaderProgram *program, m_programs)
					if (program) delete program;
				if (m_vertexShader)
					delete m_vertexShader;
				foreach (QGLShader *shader, m_fragmentShaders)
					if (shader) delete shader;
				foreach (Helpers::OpenGL::GLRenderTargetCube *rt, m_cubemaps)
					if (rt) delete rt;
				if (m_environmentShader)
					delete m_environmentShader;
				if (m_environmentProgram)
					delete m_environmentProgram;
			}

            QTime m_time;
            int m_lastTime;
            int m_mouseEventTime;
            int m_distExp;
            int frame;
            int m_maxTextureSize;

            int m_currentShader;
            int m_currentTexture;
            bool m_dynamicCubemap;
            bool m_updateAllCubemaps;


            Entity *m_itemDialog;
            QTimer *m_timer;
            Helpers::OpenGL::GLRoundedBox *m_box;
            TrackBall m_trackBalls[3];
            //QVector<GLTexture *> m_textures;
            Helpers::OpenGL::GLTextureCube *m_environment;
            Helpers::OpenGL::GLTexture3D *m_noise;
            Helpers::OpenGL::GLRenderTargetCube *m_mainCubemap;
            QVector<Helpers::OpenGL::GLRenderTargetCube *> m_cubemaps;
            QVector<QGLShaderProgram *> m_programs;
            QGLShader *m_vertexShader;
            QVector<QGLShader *> m_fragmentShaders;
            QGLShader *m_environmentShader;
            QGLShaderProgram *m_environmentProgram;

		};

		Scene::Scene(int width, int height, int maxTextureSize) :
				O_PRIVATE_CREATE(Scene)
		{

			LID << QString("Create Scene: width:%1 height:%2").arg(width).arg(height);

			O_D(Scene);
			d->m_maxTextureSize = maxTextureSize;

			setSceneRect(0, 0, width, height);


            d->m_trackBalls[0] = TrackBall(0.05f, QVector3D(0, 1, 0), TrackBall::Sphere);
            d->m_trackBalls[1] = TrackBall(0.005f, QVector3D(0, 0, 1), TrackBall::Sphere);
            d->m_trackBalls[2] = TrackBall(0.0f, QVector3D(0, 1, 0), TrackBall::Plane);


            addItem(new QtBox(64, width - 64, height - 64));
            addItem(new QtBox(64, width - 64, 64));
            addItem(new QtBox(64, 64, height - 64));
            addItem(new GraphicsEntity);

            initGL();

            d->m_timer = new QTimer(this);
            d->m_timer->setInterval(20);
            connect(d->m_timer, SIGNAL(timeout()), this, SLOT(update()));
            d->m_timer->start();

            d->m_time.start();

		}

		Scene::~Scene()
		{
			O_PRIVATE_DELETE;
		}

		void Scene::initGL()
		{
			LID << "Initialize OpenGL";
			O_D(Scene);
			d->m_box = new Helpers::OpenGL::GLRoundedBox(0.15f, 1.0f, 10);

			d->m_vertexShader = new QGLShader(QGLShader::Vertex);
			d->m_vertexShader->compileSourceFile(QLatin1String(":/res/boxes/basic.vsh"));

			QStringList list;
			list << ":/res/boxes/cubemap_posx.jpg" << ":/res/boxes/cubemap_negx.jpg" << ":/res/boxes/cubemap_posy.jpg"
					<< ":/res/boxes/cubemap_negy.jpg" << ":/res/boxes/cubemap_posz.jpg" << ":/res/boxes/cubemap_negz.jpg";
			d->m_environment = new Helpers::OpenGL::GLTextureCube(list, qMin(1024, d->m_maxTextureSize));
			d->m_environmentShader = new QGLShader(QGLShader::Fragment);
			d->m_environmentShader->compileSourceCode(environmentShaderText);
			d->m_environmentProgram = new QGLShaderProgram;
			d->m_environmentProgram->addShader(d->m_vertexShader);
			d->m_environmentProgram->addShader(d->m_environmentShader);
			d->m_environmentProgram->link();



			const int NOISE_SIZE = 128; // for a different size, B and BM in fbm.c must also be changed
			d->m_noise = new Helpers::OpenGL::GLTexture3D(NOISE_SIZE, NOISE_SIZE, NOISE_SIZE);
			QRgb *data = new QRgb[NOISE_SIZE * NOISE_SIZE * NOISE_SIZE];
			memset(data, 0, NOISE_SIZE * NOISE_SIZE * NOISE_SIZE * sizeof(QRgb));
			QRgb *p = data;
			float pos[3];
			for (int k = 0; k < NOISE_SIZE; ++k) {
				pos[2] = k * (0x20 / (float)NOISE_SIZE);
				for (int j = 0; j < NOISE_SIZE; ++j) {
					for (int i = 0; i < NOISE_SIZE; ++i) {
						for (int byte = 0; byte < 4; ++byte) {
							pos[0] = (i + (byte & 1) * 16) * (0x20 / (float)NOISE_SIZE);
							pos[1] = (j + (byte & 2) * 8) * (0x20 / (float)NOISE_SIZE);
							*p |= (int)(128.0f * (noise3(pos) + 1.0f)) << (byte * 8);
						}
						++p;
					}
				}
			}
			d->m_noise->load(NOISE_SIZE, NOISE_SIZE, NOISE_SIZE, data);
			delete[] data;




			d->m_mainCubemap = new Helpers::OpenGL::GLRenderTargetCube(512);

			QStringList filter;
			QList<QFileInfo> files;

			// Load all .png files as textures
			d->m_currentTexture = 0;
			filter = QStringList("*.png");
			files = QDir(":/res/boxes/").entryInfoList(filter, QDir::Files | QDir::Readable);

			int id = 0;
			foreach (QFileInfo file, files) {

//				GLTexture *texture = new GLTexture2D(file.absoluteFilePath(), qMin(256, d->m_maxTextureSize), qMin(256, d->m_maxTextureSize));
//				if (texture->failed()) {
//					delete texture;
//					continue;
//				}
//				d->m_textures << texture;
				LIT << "Loading Texture:" << file.absoluteFilePath();
				Textures::instance()->append(id++, file.absoluteFilePath());
			}

//			if (d->m_textures.size() == 0)
//				d->m_textures << new GLTexture2D(qMin(64, d->m_maxTextureSize), qMin(64, d->m_maxTextureSize));

			// Load all .fsh files as fragment shaders

			LID << "Loading Shaders:";

			d->m_currentShader = 3;
			filter = QStringList("*.fsh");
			files = QDir(":/res/boxes/").entryInfoList(filter, QDir::Files | QDir::Readable);
			foreach (QFileInfo file, files) {

				LIT << file.absoluteFilePath();

				QGLShaderProgram *program = new QGLShaderProgram;
				QGLShader* shader = new QGLShader(QGLShader::Fragment);
				shader->compileSourceFile(file.absoluteFilePath());
				// The program does not take ownership over the shaders, so store them in a vector so they can be deleted afterwards.
				program->addShader(d->m_vertexShader);
				program->addShader(shader);


				if (!program->link()) {
					LIW << "Failed to compile and link shader program";
					LIW << "Vertex shader log:";
					LIW << d->m_vertexShader->log();
					LIW << "Fragment shader log ( file =" << file.absoluteFilePath() << "):";
					LIW << shader->log();
					LIW << "Shader program log:";
					LIW << program->log();

					delete shader;
					delete program;
					continue;
				}				

				d->m_fragmentShaders << shader;
				d->m_programs << program;

				program->bind();
				d->m_cubemaps << ((program->uniformLocation("env") != -1) ? new Helpers::OpenGL::GLRenderTargetCube(qMin(256, d->m_maxTextureSize)) : 0);
				program->release();
			}

			if (d->m_programs.size() == 0)
			{
				LIW << "Shader programms list is empty, create default";
				d->m_programs << new QGLShaderProgram;
			}
		}


		// If one of the boxes should not be rendered, set excludeBox to its index.
		// If the main box should not be rendered, set excludeBox to -1.
		void Scene::render(const QMatrix4x4 &view)
		{
			O_D(Scene);
			QMatrix4x4 invView = view.inverted();

			// If multi-texturing is supported, use three saplers.
			if (glActiveTexture) {
				glActiveTexture(GL_TEXTURE0);
				Textures::instance()->bind(d->m_currentTexture);
				//d->m_textures[d->m_currentTexture]->bind();
				glActiveTexture(GL_TEXTURE2);
				d->m_noise->bind();
				glActiveTexture(GL_TEXTURE1);
			} else {
				Textures::instance()->bind(d->m_currentTexture);
			}

			glDisable(GL_LIGHTING);
			glDisable(GL_CULL_FACE);

			QMatrix4x4 viewRotation(view);
			viewRotation(3, 0) = viewRotation(3, 1) = viewRotation(3, 2) = 0.0f;
			viewRotation(0, 3) = viewRotation(1, 3) = viewRotation(2, 3) = 0.0f;
			viewRotation(3, 3) = 1.0f;
			Helpers::OpenGL::loadMatrix(viewRotation);
			glScalef(20.0f, 20.0f, 20.0f);

			// Don't render the environment if the environment texture can't be set for the correct sampler.
			if (glActiveTexture) {
				d->m_environment->bind();
				d->m_environmentProgram->bind();
				d->m_environmentProgram->setUniformValue("tex", GLint(0));
				d->m_environmentProgram->setUniformValue("env", GLint(1));
				d->m_environmentProgram->setUniformValue("noise", GLint(2));
				d->m_box->draw();
				d->m_environmentProgram->release();
				d->m_environment->unbind();
			}

			Helpers::OpenGL::loadMatrix(view);

			glEnable(GL_CULL_FACE);
			glEnable(GL_LIGHTING);

			for (int i = 0; i < d->m_programs.size(); ++i) {

				glPushMatrix();
				QMatrix4x4 m;
				m.rotate(d->m_trackBalls[1].rotation());

				m.rotate(360.0f * i / d->m_programs.size(), QVector3D(0.0f, 0.0f, 1.0f));
				m.translate(2.0f, 1.0f, -1.5f);
				m.scale(0.3f, 0.6f, 0.6f);
				Helpers::OpenGL::multMatrix(m);

				if (glActiveTexture) {
					if (d->m_dynamicCubemap && d->m_cubemaps[i])
						d->m_cubemaps[i]->bind();
					else
						d->m_environment->bind();
				}
				d->m_programs[i]->bind();
				d->m_programs[i]->setUniformValue("tex", GLint(0));
				d->m_programs[i]->setUniformValue("env", GLint(1));
				d->m_programs[i]->setUniformValue("noise", GLint(2));
				d->m_programs[i]->setUniformValue("view", view);
				d->m_programs[i]->setUniformValue("invView", invView);
				d->m_box->draw();
				d->m_programs[i]->release();

				if (glActiveTexture) {
					if (d->m_dynamicCubemap && d->m_cubemaps[i])
						d->m_cubemaps[i]->unbind();
					else
						d->m_environment->unbind();
				}
				glPopMatrix();
			}


			QMatrix4x4 m;
			m.rotate(d->m_trackBalls[0].rotation());
			Helpers::OpenGL::multMatrix(m);

			if (glActiveTexture) {
				if (d->m_dynamicCubemap)
					d->m_mainCubemap->bind();
				else
					d->m_environment->bind();
			}

			d->m_programs[d->m_currentShader]->bind();
			d->m_programs[d->m_currentShader]->setUniformValue("tex", GLint(0));
			d->m_programs[d->m_currentShader]->setUniformValue("env", GLint(1));
			d->m_programs[d->m_currentShader]->setUniformValue("noise", GLint(2));
			d->m_programs[d->m_currentShader]->setUniformValue("view", view);
			d->m_programs[d->m_currentShader]->setUniformValue("invView", invView);
			d->m_box->draw();
			d->m_programs[d->m_currentShader]->release();

			if (glActiveTexture) {
				if (d->m_dynamicCubemap)
					d->m_mainCubemap->unbind();
				else
					d->m_environment->unbind();
			}


			if (glActiveTexture) {
				glActiveTexture(GL_TEXTURE2);
				d->m_noise->unbind();
				glActiveTexture(GL_TEXTURE0);
			}
			Textures::instance()->unbind();
//			d->m_textures[d->m_currentTexture]->unbind();
		}

		void Scene::setStates()
		{
			//glClearColor(0.25f, 0.25f, 0.5f, 1.0f);
			O_D(Scene);
			glEnable(GL_DEPTH_TEST);
			glEnable(GL_CULL_FACE);
			glEnable(GL_LIGHTING);
			//glEnable(GL_COLOR_MATERIAL);
			glEnable(GL_TEXTURE_2D);
			glEnable(GL_NORMALIZE);

			glMatrixMode(GL_PROJECTION);
			glPushMatrix();
			glLoadIdentity();

			glMatrixMode(GL_MODELVIEW);
			glPushMatrix();
			glLoadIdentity();

			setLights();

			float materialSpecular[] = {0.5f, 0.5f, 0.5f, 1.0f};
			glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, materialSpecular);
			glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 32.0f);
		}

		void Scene::setLights()
		{
			O_D(Scene);
			glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
			float lightColour[] = {1.0f, 1.0f, 1.0f, 1.0f};
			float lightDir[] = {0.0f, 0.0f, 1.0f, 0.0f};
			glLightfv(GL_LIGHT0, GL_DIFFUSE, lightColour);
			//glLightfv(GL_LIGHT0, GL_SPECULAR, lightColour);
			//glLightfv(GL_LIGHT0, GL_POSITION, lightDir);
			glLightModelf(GL_LIGHT_MODEL_LOCAL_VIEWER, 1.0f);
			glEnable(GL_LIGHT0);

		}

		void Scene::defaultStates()
		{
			O_D(Scene);
			//glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

			glDisable(GL_DEPTH_TEST);
			glDisable(GL_CULL_FACE);
			glDisable(GL_LIGHTING);
			//glDisable(GL_COLOR_MATERIAL);
			glDisable(GL_TEXTURE_2D);
			glDisable(GL_LIGHT0);
			glDisable(GL_NORMALIZE);

			glMatrixMode(GL_MODELVIEW);
			glPopMatrix();

			glMatrixMode(GL_PROJECTION);
			glPopMatrix();

			glLightModelf(GL_LIGHT_MODEL_LOCAL_VIEWER, 0.0f);
			float defaultMaterialSpecular[] = {0.0f, 0.0f, 0.0f, 1.0f};
			glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, defaultMaterialSpecular);
			glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 0.0f);
		}

//		void Scene::renderCubemaps()
//		{
//			O_D(Scene);
//			// To speed things up, only update the cubemaps for the small cubes every N frames.
//			const int N = (d->m_updateAllCubemaps ? 1 : 3);

//			QMatrix4x4 mat;
//			GLRenderTargetCube::getProjectionMatrix(mat, 0.1f, 100.0f);

//			glMatrixMode(GL_PROJECTION);
//			glPushMatrix();
//			Helpers::OpenGL::loadMatrix(mat);

//			glMatrixMode(GL_MODELVIEW);
//			glPushMatrix();

//			QVector3D center;

//			for (int i = d->frame % N; i < d->m_cubemaps.size(); i += N) {
//				if (0 == d->m_cubemaps[i])
//					continue;

//				float angle = 2.0f * PI * i / d->m_cubemaps.size();

//				center = d->m_trackBalls[1].rotation().rotatedVector(QVector3D(cos(angle), sin(angle), 0.0f));

//				for (int face = 0; face < 6; ++face) {
//					d->m_cubemaps[i]->begin(face);

//					GLRenderTargetCube::getViewMatrix(mat, face);
//					QVector4D v = QVector4D(-center.x(), -center.y(), -center.z(), 1.0);
//					mat.setColumn(3, mat * v);

//					glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//					render(mat, i);

//					d->m_cubemaps[i]->end();
//				}
//			}

//			for (int face = 0; face < 6; ++face) {
//				d->m_mainCubemap->begin(face);
//				GLRenderTargetCube::getViewMatrix(mat, face);

//				glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//				render(mat, -1);

//				d->m_mainCubemap->end();
//			}

//			glPopMatrix();

//			glMatrixMode(GL_PROJECTION);
//			glPopMatrix();

//			d->m_updateAllCubemaps = false;
//		}

		void Scene::drawBackground(QPainter *painter, const QRectF &)
		{
			O_D(Scene);
			float width = float(painter->device()->width());
			float height = float(painter->device()->height());

			painter->beginNativePainting();
			setStates();

			//if (d->m_dynamicCubemap)
				//renderCubemaps();

			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

			glMatrixMode(GL_PROJECTION);
			gluPerspective(60.0, width / height, 0.01, 15.0);

			glMatrixMode(GL_MODELVIEW);

			QMatrix4x4 view;
			view.rotate(d->m_trackBalls[2].rotation());
			view(2, 3) -= 2.0f * exp(d->m_distExp / 1200.0f);
			render(view);

			defaultStates();
			d->frame++;

			painter->endNativePainting();
		}

		QPointF Scene::pixelPosToViewPos(const QPointF& p)
		{
			O_D(Scene);
			return QPointF(2.0 * float(p.x()) / width() - 1.0,
						   1.0 - 2.0 * float(p.y()) / height());
		}

		void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
		{
			O_D(Scene);
			QGraphicsScene::mouseMoveEvent(event);
			return;
			if (event->isAccepted())
				return;

			if (event->buttons() & Qt::LeftButton) {
				d->m_trackBalls[0].move(pixelPosToViewPos(event->scenePos()), d->m_trackBalls[2].rotation().conjugate());				
				event->accept();				
			} else {
				d->m_trackBalls[0].release(pixelPosToViewPos(event->scenePos()), d->m_trackBalls[2].rotation().conjugate());
			}

			if (event->buttons() & Qt::RightButton) {
				d->m_trackBalls[1].move(pixelPosToViewPos(event->scenePos()), d->m_trackBalls[2].rotation().conjugate());
				event->accept();
			} else {
				d->m_trackBalls[1].release(pixelPosToViewPos(event->scenePos()), d->m_trackBalls[2].rotation().conjugate());
			}

			if (event->buttons() & Qt::MidButton) {
				d->m_trackBalls[2].move(pixelPosToViewPos(event->scenePos()), QQuaternion());
				event->accept();
			} else {
				d->m_trackBalls[2].release(pixelPosToViewPos(event->scenePos()), QQuaternion());
			}
		}

		void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event)
		{
			O_D(Scene);
			QGraphicsScene::mousePressEvent(event);
			return;
			if (event->isAccepted())
				return;

			if (event->buttons() & Qt::LeftButton) {
				d->m_trackBalls[0].push(pixelPosToViewPos(event->scenePos()), d->m_trackBalls[2].rotation().conjugate());
				event->accept();
			}

			if (event->buttons() & Qt::RightButton) {
				d->m_trackBalls[1].push(pixelPosToViewPos(event->scenePos()), d->m_trackBalls[2].rotation().conjugate());
				event->accept();
			}

			if (event->buttons() & Qt::MidButton) {
				d->m_trackBalls[2].push(pixelPosToViewPos(event->scenePos()), QQuaternion());
				event->accept();
			}
		}

		void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
		{
			O_D(Scene);
			QGraphicsScene::mouseReleaseEvent(event);
			return;
			if (event->isAccepted())
				return;

			if (event->button() == Qt::LeftButton) {
				d->m_trackBalls[0].release(pixelPosToViewPos(event->scenePos()), d->m_trackBalls[2].rotation().conjugate());
				event->accept();
			}

			if (event->button() == Qt::RightButton) {
				d->m_trackBalls[1].release(pixelPosToViewPos(event->scenePos()), d->m_trackBalls[2].rotation().conjugate());
				event->accept();
			}

			if (event->button() == Qt::MidButton) {
				d->m_trackBalls[2].release(pixelPosToViewPos(event->scenePos()), QQuaternion());
				event->accept();
			}
		}

		void Scene::wheelEvent(QGraphicsSceneWheelEvent * event)
		{
			O_D(Scene);
			QGraphicsScene::wheelEvent(event);
			if (!event->isAccepted()) {
				d->m_distExp += event->delta();
				if (d->m_distExp < -8 * 120)
					d->m_distExp = -8 * 120;
				if (d->m_distExp > 10 * 120)
					d->m_distExp = 10 * 120;
				event->accept();
			}
		}

		void Scene::setShader(int index)
		{
			O_D(Scene);
			if (index >= 0 && index < d->m_fragmentShaders.size())
				d->m_currentShader = index;
		}

		void Scene::setTexture(int index)
		{
			O_D(Scene);
//			if (index >= 0 && index < d->m_textures.size())
//				d->m_currentTexture = index;
		}

		void Scene::toggleDynamicCubemap(int state)
		{
			O_D(Scene);
			if ((d->m_dynamicCubemap = (state == Qt::Checked)))
				d->m_updateAllCubemaps = true;
		}

		void Scene::setColorParameter(const QString &name, QRgb color)
		{
			O_D(Scene);
			// set the color in all programs
			foreach (QGLShaderProgram *program, d->m_programs) {
				program->bind();
				program->setUniformValue(program->uniformLocation(name), QColor(color));
				program->release();
			}
		}

		void Scene::setFloatParameter(const QString &name, float value)
		{
			O_D(Scene);
			// set the color in all programs
			foreach (QGLShaderProgram *program, d->m_programs) {
				program->bind();
				program->setUniformValue(program->uniformLocation(name), value);
				program->release();
			}
		}

		void Scene::newItem()
		{
			O_D(Scene);
			QSize size = sceneRect().size().toSize();
			addItem(new QtBox(64, rand() % (size.width() - 64) + 32, rand() % (size.height() - 64) + 32));

		}

    };};
